# -*- coding: utf-8 -*-
"""
xfree.forum.models

它提供了论坛所需的模型。

:copyright: (c) 2014 by the xfree Team.
:license: BSD，更多细节见LICENSE。
"""

import logging
from datetime import timedelta

from flask import abort, current_app, url_for
from sqlalchemy.orm import aliased

from xfree.extensions import db
from xfree.utils.database import (
    CRUDMixin,
    HideableCRUDMixin,
    UTCDateTime,
    make_comparable,
)
from xfree.utils.helpers import (
    get_categories_and_forums,
    get_forums,
    slugify,
    time_utcnow,
    topic_is_unread,
)
from xfree.utils.settings import xfree_config

# 获取日志记录器
logger = logging.getLogger(__name__)

# 定义多对多关系表 - 版主
moderators = db.Table(
    "moderators",
    db.Column(
        "user_id",
        db.Integer(),
        db.ForeignKey("users.id", ondelete="CASCADE"),
        nullable=False,
    ),
    db.Column(
        "forum_id",
        db.Integer(),
        db.ForeignKey("forums.id", ondelete="CASCADE"),
        nullable=False,
    ),
)

# 定义多对多关系表 - 主题跟踪
topictracker = db.Table(
    "topictracker",
    db.Column(
        "user_id",
        db.Integer(),
        db.ForeignKey("users.id", ondelete="CASCADE"),
        nullable=False,
    ),
    db.Column(
        "topic_id",
        db.Integer(),
        db.ForeignKey("topics.id", ondelete="CASCADE"),
        nullable=False,
    ),
)

# 定义多对多关系表 - 论坛组
forumgroups = db.Table(
    "forumgroups",
    db.Column(
        "group_id",
        db.Integer(),
        db.ForeignKey("groups.id", ondelete="CASCADE"),
        nullable=False,
    ),
    db.Column(
        "forum_id",
        db.Integer(),
        db.ForeignKey("forums.id", ondelete="CASCADE"),
        nullable=False,
    ),
)

#1###########################################
class TopicsRead(db.Model, CRUDMixin):
    """
    用于记录用户已读主题的模型类。
    """
    __tablename__ = "topicsread"

    user_id = db.Column(
        db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), primary_key=True
    )
    user = db.relationship("User", uselist=False, foreign_keys=[user_id])
    topic_id = db.Column(
        db.Integer, db.ForeignKey("topics.id", ondelete="CASCADE"), primary_key=True
    )
    topic = db.relationship("Topic", uselist=False, foreign_keys=[topic_id])
    forum_id = db.Column(
        db.Integer, db.ForeignKey("forums.id", ondelete="CASCADE"), primary_key=True
    )
    forum = db.relationship("Forum", uselist=False, foreign_keys=[forum_id])
    last_read = db.Column(
        UTCDateTime(timezone=True), default=time_utcnow, nullable=False
    )


#2###########################################
class ForumsRead(db.Model, CRUDMixin):
    """
    用于记录用户c。
    """
    __tablename__ = "forumsread"

    user_id = db.Column(
        db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), primary_key=True
    )
    user = db.relationship("User", uselist=False, foreign_keys=[user_id])
    forum_id = db.Column(
        db.Integer, db.ForeignKey("forums.id", ondelete="CASCADE"), primary_key=True
    )
    forum = db.relationship("Forum", uselist=False, foreign_keys=[forum_id])
    last_read = db.Column(
        UTCDateTime(timezone=True), default=time_utcnow, nullable=False
    )
    cleared = db.Column(UTCDateTime(timezone=True), nullable=True)


#3###########################################
@make_comparable
class Report(db.Model, CRUDMixin):
    """
    举报模型类。
    """
    __tablename__ = "reports"

    # TODO: 除了下面的信息，还存储主题标题和用户名。
    # 这样，如果用户或帖子被删除，我们仍然可以查看举报
    id = db.Column(db.Integer, primary_key=True)
    reporter_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
    reported = db.Column(
        UTCDateTime(timezone=True), default=time_utcnow, nullable=False
    )
    post_id = db.Column(db.Integer, db.ForeignKey("posts.id"), nullable=True)
    zapped = db.Column(UTCDateTime(timezone=True), nullable=True)
    zapped_by = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
    reason = db.Column(db.Text, nullable=True)

    post = db.relationship(
        "Post",
        lazy="joined",
        backref=db.backref("report", cascade="all, delete-orphan"),
    )
    reporter = db.relationship("User", lazy="joined", foreign_keys=[reporter_id])
    zapper = db.relationship("User", lazy="joined", foreign_keys=[zapped_by])

    def __repr__(self):
        return "<{} {}>".format(self.__class__.__name__, self.id)

    def save(self, post=None, user=None):
        """
        保存举报信息。

        :param post: 被举报的帖子
        :param user: 举报的用户
        :return: 保存后的举报对象
        """
        if self.id:
            db.session.add(self)
            db.session.commit()
            return self

        if post and user:
            self.reporter = user
            self.reported = time_utcnow()
            self.post = post

        db.session.add(self)
        db.session.commit()
        return self

#4###########################################
@make_comparable
class Post(HideableCRUDMixin, db.Model):
    """
    帖子模型类。
    """
    __tablename__ = "posts"

    id = db.Column(db.Integer, primary_key=True)
    topic_id = db.Column(
        db.Integer,
        db.ForeignKey("topics.id", ondelete="CASCADE", use_alter=True),
        nullable=True,
    )
    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
    username = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)
    date_created = db.Column(
        UTCDateTime(timezone=True), default=time_utcnow, nullable=False
    )
    date_modified = db.Column(UTCDateTime(timezone=True), nullable=True)
    modified_by = db.Column(db.String(200), nullable=True)

    # 特性
    @property
    def url(self):
        """
        返回帖子的URL。
        """
        return url_for("forum.view_post", post_id=self.id)

    # 方法
    def __init__(self, content=None, user=None, topic=None):
        """
        创建一个带有初始值的帖子对象。

        :param content: 帖子内容
        :param user: 帖子的用户
        :param topic: 可以是主题ID或主题对象
        """
        if content:
            self.content = content

        if user:
            # 在这里设置用户 - 即使显式设置用户ID，出于某种原因也会破坏批量插入
            self.user_id = user.id
            self.username = user.username

        if topic:
            self.topic_id = topic if isinstance(topic, int) else topic.id

        self.date_created = time_utcnow()

    def __repr__(self):
        """
        设置为数据库中对象的唯一键。
        这是cache.memoize()在跨请求时工作所必需的。
        """
        return "<{} {}>".format(self.__class__.__name__, self.id)

    def is_first_post(self):
        """
        检查此帖子是否是主题中的第一篇帖子。
        """
        return self.topic.is_first_post(self)

    def save(self, user=None, topic=None):
        """
        保存新帖子。如果没有传递参数，我们假设你只是要更新现有帖子。
        操作成功后返回对象。

        :param user: 创建帖子的用户
        :param topic: 创建帖子的主题
        :return: 保存后的帖子对象
        """
        current_app.pluggy.hook.xfree_event_post_save_before(post=self)

        # 更新帖子
        if self.id:
            db.session.add(self)
            db.session.commit()
            current_app.pluggy.hook.xfree_event_post_save_after(
                post=self, is_new=False
            )
            return self

        # 添加新帖子
        if user and topic:
            with db.session.no_autoflush:
                created = time_utcnow()
                self.user = user
                self.username = user.username
                self.topic = topic
                self.date_created = created

                if not topic.hidden:
                    topic.last_updated = created
                    topic.last_post = self

                    # 更新论坛的最后帖子信息
                    topic.forum.last_post = self
                    topic.forum.last_post_user = self.user
                    topic.forum.last_post_title = topic.title
                    topic.forum.last_post_username = user.username
                    topic.forum.last_post_created = created

                    # 更新帖子计数
                    user.post_count += 1
                    topic.post_count += 1
                    topic.forum.post_count += 1

            # 提交！
            db.session.add(self)
            db.session.commit()
            current_app.pluggy.hook.xfree_event_post_save_after(
                post=self, is_new=True
            )
            return self

    def delete(self):
        """
        删除帖子并返回自身。
        """
        # 这将删除整个主题
        if self.topic.first_post == self:
            self.topic.delete()
            return self

        db.session.delete(self)

        self._deal_with_last_post()
        self._update_counts()

        db.session.commit()
        return self

    def hide(self, user):
        if self.hidden:
            return

        if self.topic.first_post == self:
            self.topic.hide(user)
            return self

        super(Post, self).hide(user)
        self._deal_with_last_post()
        self._update_counts()
        db.session.commit()
        return self

    def unhide(self):
        if not self.hidden:
            return

        if self.topic.first_post == self:
            self.topic.unhide()
            return self

        self._restore_post_to_topic()
        super(Post, self).unhide()
        self._update_counts()
        db.session.commit()
        return self

    def _deal_with_last_post(self):
        if self.topic.last_post == self:
            # 更新论坛中的最后帖子
            if self.topic.last_post == self.topic.forum.last_post:
                # 这里我们需要论坛中的倒数第二帖子，
                # 因为最后帖子将被删除
                second_last_post = (
                    Post.query.filter(
                        Post.topic_id == Topic.id,
                        Topic.forum_id == self.topic.forum.id,
                        Post.hidden!= True,
                        Post.id!= self.id,
                    )
                   .order_by(Post.id.desc())
                   .limit(1)
                   .first()
                )

                if second_last_post:
                    # 现在让我们将倒数第二帖子更新为最后帖子
                    self.topic.forum.last_post = second_last_post
                    self.topic.forum.last_post_title = second_last_post.topic.title
                    self.topic.forum.last_post_user = second_last_post.user
                    self.topic.forum.last_post_username = second_last_post.username
                    self.topic.forum.last_post_created = second_last_post.date_created
                else:
                    self.topic.forum.last_post = None
                    self.topic.forum.last_post_title = None
                    self.topic.forum.last_post_user = None
                    self.topic.forum.last_post_username = None
                    self.topic.forum.last_post_created = None

            # 检查这个主题中是否有倒数第二帖子
            if self.topic.second_last_post is not None:
                # 现在倒数第二帖子将成为最后帖子
                self.topic.last_post_id = self.topic.second_last_post

            # 没有倒数第二帖子，现在最后帖子也是第一篇帖子
            else:
                self.topic.last_post = self.topic.first_post

            self.topic.last_updated = self.topic.last_post.date_created

    def _update_counts(self):
        if self.hidden:
            clauses = [Post.hidden!= True, Post.id!= self.id]
        else:
            clauses = [db.or_(Post.hidden!= True, Post.id == self.id)]

        user_post_clauses = clauses + [
            Post.user_id == self.user.id,
            Topic.id == Post.topic_id,
            Topic.hidden!= True,
        ]

        # 更新帖子计数
        self.user.post_count = Post.query.filter(*user_post_clauses).count()

        if self.topic.hidden:
            self.topic.post_count = 0
        else:
            topic_post_clauses = clauses + [
                Post.topic_id == self.topic.id,
            ]
            self.topic.post_count = Post.query.filter(*topic_post_clauses).count()

        forum_post_clauses = clauses + [
            Post.topic_id == Topic.id,
            Topic.forum_id == self.topic.forum.id,
            Topic.hidden!= True,
        ]

        self.topic.forum.post_count = Post.query.filter(*forum_post_clauses).count()

    def _restore_post_to_topic(self):
        last_unhidden_post = (
            Post.query.filter(
                Post.topic_id == self.topic_id,
                Post.id!= self.id,
                Post.hidden!= True,
            )
           .limit(1)
           .first()
        )

        # 应该永远不会是None，但为了安全起见还是处理一下
        if last_unhidden_post and self.date_created > last_unhidden_post.date_created:
            self.topic.last_post = self
            self.second_last_post = last_unhidden_post

            # 如果我们再次是主题中最新的，我们可能再次是论坛中最新的
            # 仅当我们的父主题未隐藏时设置
            if not self.topic.hidden and (
                not self.topic.forum.last_post
                or self.date_created > self.topic.forum.last_post.date_created
            ):
                self.topic.forum.last_post = self
                self.topic.forum.last_post_title = self.topic.title
                self.topic.forum.last_post_user = self.user
                self.topic.forum.last_post_username = self.username
                self.topic.forum.last_post_created = self.date_created


#5###########################################
@make_comparable
class Topic(HideableCRUDMixin, db.Model):
    """
    主题模型类，用于表示论坛中的主题。

    包含了主题的各种属性信息，如标题、创建者、创建时间、更新时间、
    锁定状态、重要性、浏览量、帖子数量等，还定义了与帖子的关系。
    """
    __tablename__ = "topics"

    # 主题的唯一标识符
    id = db.Column(db.Integer, primary_key=True)
    # 主题所属论坛的ID，关联forums表，若论坛被删除则主题也会被删除
    forum_id = db.Column(db.Integer, db.ForeignKey("forums.id", ondelete="CASCADE"), nullable=False)
    # 主题的标题，不能为空
    title = db.Column(db.String(255), nullable=False)
    # 创建主题的用户ID，关联users表，可为空
    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
    # 创建主题的用户名，不能为空
    username = db.Column(db.String(200), nullable=False)
    # 主题的创建时间，默认为当前时间，不能为空
    date_created = db.Column(UTCDateTime(timezone=True), default=time_utcnow, nullable=False)
    # 主题的最后更新时间，默认为当前时间，不能为空
    last_updated = db.Column(UTCDateTime(timezone=True), default=time_utcnow, nullable=False)
    # 主题是否被锁定，默认为False
    locked = db.Column(db.Boolean, default=False, nullable=False)
    # 主题是否重要，默认为False
    important = db.Column(db.Boolean, default=False, nullable=False)
    # 主题的浏览量，默认为0
    views = db.Column(db.Integer, default=0, nullable=False)
    # 主题中的帖子数量，默认为0
    post_count = db.Column(db.Integer, default=0, nullable=False)

    # 与主题中第一篇帖子的一对一关系（uselist=False表示一对一）
    first_post_id = db.Column(db.Integer, db.ForeignKey("posts.id", ondelete="CASCADE"), nullable=True)
    first_post = db.relationship("Post", backref="first_post", uselist=False, foreign_keys=[first_post_id])

    # 与主题中最后一篇帖子的一对一关系
    last_post_id = db.Column(db.Integer, db.ForeignKey("posts.id"), nullable=True)
    last_post = db.relationship("Post", backref="last_post", uselist=False, foreign_keys=[last_post_id])

    # 与主题相关的所有帖子的一对多关系
    posts = db.relationship(
        "Post",
        backref="topic",
        lazy="dynamic",
        primaryjoin="Post.topic_id == Topic.id",
        cascade="all, delete-orphan",
        post_update=True
    )

    @property
    def second_last_post(self):
        """
        获取主题中的倒数第二篇帖子的ID，如果不存在则返回None。
        """
        try:
            return self.posts[-2].id
        except IndexError:
            return None

    @property
    def slug(self):
        """
        返回主题标题的slug化版本（用于生成友好的URL）。
        """
        return slugify(self.title)

    @property
    def url(self):
        """
        返回主题的slug化URL。
        如果没有slug化版本，则返回基于主题ID的URL。
        """
        if not self.slug:
            return url_for("forum.view_topic", topic_id=self.id)
        return url_for("forum.view_topic", topic_id=self.id, slug=self.slug)

    def __init__(self, title=None, user=None, content=None):
        """
        创建主题对象并设置初始值。

        :param title: 主题的标题
        :param user: 创建主题的用户
        :param content: 主题的初始帖子内容（可选）
        """
        if title:
            self.title = title

        if user:
            # 在这里设置用户，即使显式设置用户ID，出于某种原因也会破坏批量插入操作
            self.user_id = user.id
            self.username = user.username

        if content:
            self._post = Post(content=content)

        self.date_created = self.last_updated = time_utcnow()

    def __repr__(self):
        """
        返回一个表示主题对象的字符串，格式为<Topic {主题ID}>，
        用于调试和日志记录等。
        """
        return "<{} {}>".format(self.__class__.__name__, self.id)

    def is_first_post(self, post):
        """
        检查给定的帖子是否是主题中的第一篇帖子。

        :param post: 要检查的帖子对象
        :return: 如果是第一篇帖子则返回True，否则返回False
        """
        return self.first_post_id == post.id

    def first_unread(self, topicsread, user, forumsread=None):
        """
        返回主题中第一篇未读帖子的URL，如果没有未读帖子，则返回主题的URL。

        :param topicsread: 主题阅读状态对象
        :param user: 要检查阅读状态的用户对象
        :param forumsread: 论坛阅读状态对象（可选）
        :return: 第一篇未读帖子的URL或主题的URL
        """
        # 如果主题是未读的，则尝试获取第一篇未读帖子
        if topic_is_unread(self, topicsread, user, forumsread):
            query = Post.query.filter(Post.topic_id == self.id)
            if topicsread is not None:
                query = query.filter(Post.date_created > topicsread.last_read)
            post = query.order_by(Post.id.asc()).first()
            if post is not None:
                return post.url

        return self.url

    @classmethod
    def get_topic(cls, topic_id, user):
        """
        根据主题ID获取主题对象，如果主题不存在则返回404错误。

        :param topic_id: 要获取的主题ID
        :param user: 用户对象（当前未用到）
        :return: 主题对象
        """
        topic = Topic.query.filter_by(id=topic_id).first_or_404()
        return topic

    def tracker_needs_update(self, forumsread, topicsread):
        """
        判断主题阅读跟踪器是否需要更新。
        如果配置了TRACKER_LENGTH，只有比TRACKER_LENGTH时间内更新的主题才被认为是未读的。

        :param forumsread: 论坛阅读状态对象
        :param topicsread: 主题阅读状态对象
        :return: 如果需要更新则返回True，否则返回False
        """
        read_cutoff = None
        if xfree_config["TRACKER_LENGTH"] > 0:
            read_cutoff = time_utcnow() - timedelta(days=xfree_config["TRACKER_LENGTH"])

        # 跟踪器被禁用，不需要更新
        if read_cutoff is None:
            logger.debug("Readtracker is disabled.")
            return False

        # 主题的最后更新时间早于截止时间，不需要更新
        elif read_cutoff > self.last_post.date_created:
            logger.debug("Topic is below the read_cutoff (too old).")
            return False

        # 如果用户标记了论坛已读且之后没有新帖子，不需要更新
        if (
                forumsread
                and forumsread.cleared is not None
                and forumsread.cleared >= self.last_post.date_created
        ):
            logger.debug("User has marked the forum as read. No new posts since then.")
            return False

        # 如果主题的最后一篇帖子已经被读取，不需要更新
        if topicsread and topicsread.last_read >= self.last_post.date_created:
            logger.debug("The last post in this topic has already been read.")
            return False

        logger.debug("Topic is unread.")
        return True

    def update_read(self, user, forum, forumsread):
        """
        更新指定用户的主题阅读和论坛阅读跟踪器状态。
        如果主题有新帖子或用户未读该主题，则进行更新，并返回跟踪器是否被更新的状态。

        :param user: 用户对象
        :param forum: 主题所属的论坛对象
        :param forumsread: 论坛阅读状态对象
        :return: 如果跟踪器被更新则返回True，否则返回False
        """
        # 用户未登录，不需要更新
        if not user.is_authenticated:
            return False

        topicsread = TopicsRead.query.filter(
            TopicsRead.user_id == user.id, TopicsRead.topic_id == self.id
        ).first()

        if not self.tracker_needs_update(forumsread, topicsread):
            return False

        updated = False

        # 如果存在主题阅读记录且有新帖子未读，则更新主题阅读记录
        if topicsread:
            logger.debug("Updating existing TopicsRead '{}' object.".format(topicsread))
            topicsread.last_read = time_utcnow()
            topicsread.save()
            updated = True

        # 如果不存在主题阅读记录，则创建新的主题阅读记录
        elif not topicsread:
            logger.debug("Creating new TopicsRead object.")
            topicsread = TopicsRead()
            topicsread.user = user
            topicsread.topic = self
            topicsread.forum = self.forum
            topicsread.last_read = time_utcnow()
            topicsread.save()
            updated = True

        # 没有未读帖子，不需要更新
        else:
            updated = False

        # 更新论坛阅读跟踪器状态，并返回是否更新的结果
        updated = forum.update_read(user, forumsread, topicsread)

        return updated

    def recalculate(self):
        """
        重新计算主题中的帖子数量。
        """
        post_count = Post.query.filter_by(topic_id=self.id).count()
        self.post_count = post_count
        self.save()
        return self

    def move(self, new_forum):
        """
        将主题移动到另一个论坛。

        :param new_forum: 目标论坛对象
        :return: 如果移动成功则返回True，否则返回False
        """
        # 如果目标论坛和当前论坛相同，则不需要移动
        if self.forum == new_forum:
            return False

        old_forum = self.forum
        self.forum.post_count -= self.post_count
        self.forum.topic_count -= 1
        self.forum = new_forum

        new_forum.post_count += self.post_count
        new_forum.topic_count += 1

        db.session.commit()

        new_forum.update_last_post()
        old_forum.update_last_post()

        TopicsRead.query.filter_by(topic_id=self.id).delete()

        return True

    def save(self, user=None, forum=None, post=None):
        """
        保存主题对象。如果没有传入参数，则仅更新主题。

        :param user: 创建主题的用户对象（可选）
        :param forum: 主题所属的论坛对象（可选）
        :param post: 与主题相关的帖子对象（可选）
        :return: 保存后的主题对象
        """
        current_app.pluggy.hook.xfree_event_topic_save_before(topic=self)

        # 更新主题
        if self.id:
            db.session.add(self)
            db.session.commit()
            current_app.pluggy.hook.xfree_event_topic_save_after(topic=self, is_new=False)
            return self

        with db.session.no_autoflush:
            # 设置论坛和用户ID
            self.forum = forum
            self.user = user
            self.username = user.username

            # 设置最后更新时间（用于阅读跟踪器）
            self.date_created = self.last_updated = time_utcnow()

            # 插入并提交主题
            db.session.add(self)
            db.session.commit()

            if post is not None:
                self._post = post

            # 创建主题相关的帖子
            self._post.save(user, self)

            # 更新第一篇和最后一篇帖子的ID
            self.last_post = self.first_post = self._post

            # 更新主题数量
            forum.topic_count += 1

        db.session.commit()
        current_app.pluggy.hook.xfree_event_topic_save_after(topic=self, is_new=True)
        return self

    def delete(self):
        """
        删除主题及其相关的帖子。
        """
        forum = self.forum
        # 获取与主题相关的用户
        invovled_users = self.involved_users().all()

        db.session.delete(self)
        self._fix_user_post_counts(invovled_users)
        self._fix_post_counts(forum)

        # 如果论坛的最后一篇帖子ID为空或者等于当前主题的最后一篇帖子ID
        if forum.last_post_id is None or self.last_post_id == forum.last_post_id:
            forum.update_last_post(commit=False)

        db.session.commit()
        return self

    def hide(self, user):
        """
        隐藏主题（软删除）。

        :param user: 执行隐藏操作的用户对象
        :return: 隐藏后的主题对象
        """
        if self.hidden:
            return

        involved_users = self.involved_users().all()
        self._remove_topic_from_forum()
        super(Topic, self).hide(user)
        self._handle_first_post()
        self._fix_user_post_counts(involved_users)
        self._fix_post_counts(self.forum)
        db.session.commit()
        return self

    def unhide(self):
        """
        取消隐藏主题。

        :return: 取消隐藏后的主题对象
        """
        if not self.hidden:
            return

        involved_users = self.involved_users().all()
        super(Topic, self).unhide()
        self._handle_first_post()
        self._restore_topic_to_forum()
        self._fix_user_post_counts(involved_users)
        self.forum.recalculate()
        db.session.commit()
        return self

    def _remove_topic_from_forum(self):
        """
        从论坛中移除主题时的相关处理。
        例如更新论坛的最后一篇帖子相关信息。
        """
        # 获取论坛中除当前主题外按最后一篇帖子时间倒序排列的前两个主题
        topics = (
            Topic.query.filter(Topic.forum_id == self.forum_id, Topic.hidden != True)
            .order_by(Topic.last_post_id.desc())
            .limit(2)
            .offset(0)
            .all()
        )

        # 如果有两个主题且当前主题是最后一篇帖子对应的主题
        if len(topics) > 1:
            if topics[0] == self:
                # 将论坛的最后一篇帖子设置为另一个主题的最后一篇帖子
                self.forum.last_post = topics[1].last_post
                self.forum.last_post_title = topics[1].title
                self.forum.last_post_user = topics[1].user
                self.forum.last_post_username = topics[1].username
                self.forum.last_post_created = topics[1].last_updated
        else:
            self.forum.last_post = None
            self.forum.last_post_title = None
            self.forum.last_post_user = None
            self.forum.last_post_username = None
            self.forum.last_post_created = None

    def _fix_user_post_counts(self, users=None):
        """
        更新用户的帖子数量。

        :param users: 用户对象列表（可选）
        """
        if users:
            for user in users:
                user.post_count = Post.query.filter(
                    Post.user_id == user.id,
                    Topic.id == Post.topic_id,
                    Topic.hidden != True,
                    Post.hidden != True
                ).count()

    def _fix_post_counts(self, forum):
        """
        更新论坛的主题和帖子数量。

        :param forum: 论坛对象
        """
        clauses = [Topic.forum_id == forum.id]
        if self.hidden:
            clauses.extend([
                Topic.id != self.id,
                Topic.hidden != True
            ])
        else:
            clauses.append(db.or_(Topic.id == self.id, Topic.hidden != True))

        forum.topic_count = Topic.query.filter(*clauses).count()

        post_count = clauses + [Post.topic_id == Topic.id]

        if self.hidden:
            post_count.append(Post.hidden != True)
        else:
            post_count.append(db.or_(Post.hidden != True, Post.id == self.first_post.id))

        forum.post_count = Post.query.distinct().filter(*post_count).count()

    def _restore_topic_to_forum(self):
        """
        当取消隐藏主题时，恢复主题到论坛的相关操作。
        例如更新论坛的最后一篇帖子相关信息。
        """
        if (
                self.forum.last_post is None
                or self.forum.last_post_created < self.last_updated
        ):
            self.forum.last_post = self.last_post
            self.forum.last_post_title = self.title
            self.forum.last_post_user = self.user
            self.forum.last_post_username = self.username
            self.forum.last_post_created = self.last_updated

    def _handle_first_post(self):
        """
        处理主题中的第一篇帖子的隐藏相关属性。
        """
        # 特殊处理，防止递归调用
        self.first_post.hidden = self.hidden
        self.first_post.hidden_by = self.hidden_by
        self.first_post.hidden_at = self.hidden_at

    def involved_users(self):
        """
        获取与该主题相关的所有用户的查询。
        所谓相关用户，是指在该主题的帖子中发表过内容的用户。
        这里存在一个待解决的循环导入问题。
        """
        # 为避免循环导入，在此处导入User模型
        from xfree.user.models import User

        # 创建一个查询，用于获取用户# 添加过滤条件，筛选出在当前主题下有帖子的用户
        return User.query.distinct().filter(Post.topic_id == self.id, User.id == Post.user_id)


#6###########################################
@make_comparable
class Forum(db.Model, CRUDMixin):
    """
    论坛模型类，用于表示论坛相关的信息。

    包含论坛的基本属性、与其他模型（如主题、用户、用户组等）的关系，
    以及用于处理论坛相关操作（如更新最后一篇帖子、更新阅读状态、
    重新计算统计信息、保存、删除、移动主题等）的方法。
    """
    __tablename__ = "forums"

    # 论坛的唯一标识符
    id = db.Column(db.Integer, primary_key=True)
    # 论坛所属类别的ID，关联categories表，若类别被删除则论坛也会被删除
    category_id = db.Column(db.Integer, db.ForeignKey("categories.id", ondelete="CASCADE"), nullable=False)
    # 论坛的标题，不能为空
    title = db.Column(db.String(255), nullable=False)
    # 论坛的描述，可为空
    description = db.Column(db.Text, nullable=True)
    # 论坛的位置，默认值为1，不能为空
    position = db.Column(db.Integer, default=1, nullable=False)
    # 论坛是否被锁定，默认值为False
    locked = db.Column(db.Boolean, default=False, nullable=False)
    # 是否显示版主，默认值为False
    show_moderators = db.Column(db.Boolean, default=False, nullable=False)
    # 外部链接，可为空
    external = db.Column(db.String(200), nullable=True)

    # 论坛中的帖子数量，默认值为0，不能为空
    post_count = db.Column(db.Integer, default=0, nullable=False)
    # 论坛中的主题数量，默认值为0，不能为空
    topic_count = db.Column(db.Integer, default=0, nullable=False)

    # 与论坛中最后一篇帖子的一对一关系
    last_post_id = db.Column(db.Integer, db.ForeignKey("posts.id"), nullable=True)
    last_post = db.relationship("Post", backref="last_post_forum", uselist=False, foreign_keys=[last_post_id])

    # 最后一篇帖子的用户ID，若用户被删除则设置为NULL
    last_post_user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
    last_post_user = db.relationship("User", uselist=False, foreign_keys=[last_post_user_id])

    # 论坛最后一篇帖子的标题，若论坛没有帖子则可为NULL
    last_post_title = db.Column(db.String(255), nullable=True)
    # 论坛最后一篇帖子的用户名，若论坛没有帖子则可为NULL
    last_post_username = db.Column(db.String(255), nullable=True)
    # 论坛最后一篇帖子的创建时间，默认为当前时间，若论坛没有帖子则可为NULL
    last_post_created = db.Column(UTCDateTime(timezone=True), default=time_utcnow, nullable=True)

    # 与论坛相关的所有主题的一对多关系
    topics = db.relationship(
        "Topic", backref="forum", lazy="dynamic", cascade="all, delete-orphan"
    )

    # 与论坛版主的多对多关系
    moderators = db.relationship(
        "User",
        secondary=moderators,
        primaryjoin=(moderators.c.forum_id == id),
        backref=db.backref("forummoderator", lazy="dynamic"),
        lazy="joined"
    )
    # 与用户组的多对多关系
    groups = db.relationship(
        "Group",
        secondary=forumgroups,
        primaryjoin=(forumgroups.c.forum_id == id),
        backref="forumgroups",
        lazy="joined"
    )

    # 特性

    @property
    def slug(self):
        """
        返回论坛标题的slug化版本（用于生成友好的URL）。
        """
        return slugify(self.title)

    @property
    def url(self):
        """
        返回论坛的slug化URL。
        如果有外部链接，则返回外部链接；否则，返回基于论坛ID和slug的内部链接。
        """
        if self.external:
            return self.external
        return url_for("forum.view_forum", forum_id=self.id, slug=self.slug)

    @property
    def last_post_url(self):
        """
        返回论坛中最后一篇帖子的URL。
        """
        return url_for("forum.view_post", post_id=self.last_post_id)

    # 方法

    def __repr__(self):
        """
        返回一个表示论坛对象的字符串，格式为<Forum {论坛ID}>，
        用于调试和日志记录等。
        """
        return "<{} {}>".format(self.__class__.__name__, self.id)

    def update_last_post(self, commit=True):
        """
        更新论坛中的最后一篇帖子信息。

        通过查询获取论坛中最新的帖子，如果有新的最后一篇帖子（与当前记录的不同），
        则更新论坛的最后一篇帖子相关属性（如帖子对象、标题、用户ID、用户名、创建时间）。
        如果没有找到帖子，则将所有相关属性设置为NULL。
        如果commit为True，则提交数据库会话。
        """
        last_post = (
            Post.query.filter(Post.topic_id == Topic.id, Topic.forum_id == self.id)
           .order_by(Post.date_created.desc())
           .limit(1)
           .first()
        )

        # 当论坛中没有主题时，最后一篇帖子为None
        if last_post is not None:
            # 如果发现新的最后一篇帖子
            if last_post!= self.last_post:
                self.last_post = last_post
                self.last_post_title = last_post.topic.title
                self.last_post_user_id = last_post.user_id
                self.last_post_username = last_post.username
                self.last_post_created = last_post.date_created

        # 没有找到帖子
        else:
            self.last_post = None
            self.last_post_title = None
            self.last_post_user = None
            self.last_post_username = None
            self.last_post_created = None

        if commit:
            db.session.commit()

    def update_read(self, user, forumsread, topicsread):
        """
        更新用户对论坛的阅读状态（ForumsRead）。

        如果用户未登录或没有提供topicsread对象，则返回False。
        根据配置中的TRACKER_LENGTH计算读截止时间（read_cutoff）。
        查询论坛中未读的主题数量（unread_count），基于用户对主题和论坛的阅读状态进行判断。
        如果没有未读主题，根据forumsread的情况进行处理：
        - 如果forumsread存在且其last_read时间大于topicsread的last_read时间，返回False。
        - 如果forumsread存在，则更新其last_read时间并保存，返回True。
        - 如果forumsread不存在，则创建新的ForumsRead对象并保存，返回True。
        如果有未读主题，则返回False，表示没有更新ForumsRead对象。
        """
        if not user.is_authenticated or topicsread is None:
            return False

        read_cutoff = None
        if xfree_config["TRACKER_LENGTH"] > 0:
            read_cutoff = time_utcnow() - timedelta(days=xfree_config["TRACKER_LENGTH"])

        # 获取论坛中的未读帖子数量
        unread_count = (
            Topic.query.outerjoin(
                TopicsRead,
                db.and_(TopicsRead.topic_id == Topic.id, TopicsRead.user_id == user.id)
            )
           .outerjoin(
                ForumsRead,
                db.and_(ForumsRead.forum_id == Topic.forum_id, ForumsRead.user_id == user.id)
            )
           .filter(
                Topic.forum_id == self.id,
                Topic.last_updated > read_cutoff,
                db.or_(TopicsRead.last_read == None,  # noqa: E711
                       TopicsRead.last_read < Topic.last_updated),
                db.or_(ForumsRead.last_read == None,  # noqa: E711
                       ForumsRead.last_read < Topic.last_updated)
            )
           .count()
        )

        # 没有未读主题，尝试将论坛标记为已读
        if unread_count == 0:
            logger.debug("No unread topics. Trying to mark the forum as read.")

            if forumsread and forumsread.last_read > topicsread.last_read:
                logger.debug(
                    "forumsread.last_read is newer than "
                    "topicsread.last_read. Everything is read."
                )
                return False

            # ForumsRead对象存在，更新它
            elif forumsread:
                logger.debug("Updating existing ForumsRead '{}' object.".format(forumsread))
                forumsread.last_read = time_utcnow()
                forumsread.save()
                return True

            # 不存在ForumsRead对象，创建一个新的
            logger.debug("Creating new ForumsRead object.")
            forumsread = ForumsRead()
            forumsread.user = user
            forumsread.forum = self
            forumsread.last_read = time_utcnow()
            forumsread.save()
            return True

        # 因为还有未读主题，所以没有更新ForumsRead对象
        logger.debug(
            "No ForumsRead object updated - there are still {} "
            "unread topics.".format(unread_count)
        )
        return False

    def recalculate(self, last_post=False):
        """
        重新计算论坛中的帖子数量和主题数量。

        分别查询论坛中未隐藏的主题数量和相关的未隐藏帖子数量，更新论坛的相应统计属性。
        如果last_post为True，则调用update_last_post方法更新论坛最后一篇帖子相关信息。
        最后保存论坛对象并返回自身。
        """
        topic_count = Topic.query.filter(
            Topic.forum_id == self.id, Topic.hidden!= True
        ).count()
        post_count = Post.query.filter(
            Post.topic_id == Topic.id,
            Topic.forum_id == self.id,
            Post.hidden!= True,
            Topic.hidden!= True
        ).count()
        self.topic_count = topic_count
        self.post_count = post_count

        if last_post:
            self.update_last_post()

        self.save()
        return self

    def save(self, groups=None):
        """
        保存论坛对象。

        如果论坛对象已经有ID，则使用merge方法合并到会话中。
        如果没有ID，则在无自动刷新会话（no_autoflush）的情况下进行操作：
        - 如果没有提供groups参数，则导入Group模型并获取按名称升序排列的所有组，设置为论坛的组。
        - 将论坛对象添加到会话中。
        最后提交数据库会话并返回论坛对象。
        """
        if self.id:
            db.session.merge(self)
        else:
            with db.session.no_autoflush:
                if groups is None:
                    from xfree.user.models import Group
                    self.groups = Group.query.order_by(Group.name.asc()).all()
                db.session.add(self)

        db.session.commit()
        return self

    def delete(self, users=None):
        """
        删除论坛。

        首先从数据库中删除论坛对象并提交会话。
        如果提供了用户列表（users），则更新这些用户的帖子数量：
        - 遍历用户列表，计算每个用户的帖子数量（通过查询该用户的未隐藏帖子），
        将用户添加到列表中，最后将列表中的所有用户添加到会话并提交。
        返回论坛对象自身。
        """
        # 删除论坛
        db.session.delete(self)
        db.session.commit()

        # 更新用户的帖子数量
        if users:
            users_list = []
            for user in users:
                user.post_count = Post.query.filter_by(user_id=user.id).count()
                users_list.append(user)
            db.session.add_all(users_list)
            db.session.commit()

        return self

    def move_topics_to(self, topics):
        """
        将一组主题移动到本论坛。

        遍历主题列表，调用每个主题的move方法将其移动到本论坛。
        如果所有主题都成功移动，则返回True；否则返回False。
        """
        status = False
        for topic in topics:
            status = topic.move(self)
        return status

    # 类方法

    @classmethod
    def get_forum(cls, forum_id, user):
        """
        根据论坛ID获取论坛对象和对应的ForumsRead对象（如果用户已登录）。

        如果用户已登录，则通过查询、外连接和加载相关实体（Forum的category和ForumsRead），
        获取论坛和ForumsRead对象的元组，若找不到则返回404错误。
        如果用户未登录，则仅查询论坛对象，若找不到则返回404错误，ForumsRead对象设为None。
        返回论坛和ForumsRead对象的元组。
        """
        if user.is_authenticated:
            forum, forumsread = (
                Forum.query.filter(Forum.id == forum_id)
               .options(db.joinedload(Forum.category))
               .outerjoin(
                    ForumsRead,
                    db.and_(ForumsRead.forum_id == Forum.id, ForumsRead.user_id == user.id)
                )
               .add_entity(ForumsRead)
               .first_or_404()
            )
        else:
            forum = Forum.query.filter(Forum.id == forum_id).first_or_404()
            forumsread = None

        return forum, forumsread

    @classmethod
    def get_topics(cls, forum_id, user, page=1, per_page=20):
        """
        获取论坛中的主题列表。

        如果用户已登录，则通过查询、外连接主题的阅读状态（TopicsRead）和最后一篇帖子（Post），
        并添加相关实体，按照主题的重要性和最后更新时间降序排列，进行分页查询。
        如果用户未登录，则仅通过外连接最后一篇帖子进行分页查询，并将TopicsRead设为None。
        返回分页后的主题列表。
        """
        if user.is_authenticated:
            topics = (
                Topic.query.filter_by(forum_id=forum_id)
               .outerjoin(
                    TopicsRead,
                    db.and_(TopicsRead.topic_id == Topic.id, TopicsRead.user_id == user.id)
                )
               .outerjoin(Post, Topic.last_post_id == Post.id)
               .add_entity(Post)
               .add_entity(TopicsRead)
               .order_by(Topic.important.desc(), Topic.last_updated.desc())
               .paginate(page=page, per_page=per_page, max_per_page=None)
            )
        else:
            topics = (
                Topic.query.filter_by(forum_id=forum_id)
               .outerjoin(Post, Topic.last_post_id == Post.id)
               .add_entity(Post)
               .order_by(Topic.important.desc(), Topic.last_updated.desc())
               .paginate(page=page, per_page=per_page, max_per_page=None)
            )

            topics.items = [(topic, last_post, None) for topic, last_post in topics.items]

        return topics


#7###########################################
@make_comparable
class Category(db.Model, CRUDMixin):
    """
    类别模型类，用于表示论坛类别相关的信息。

    包含类别基本属性、与论坛的关系，以及用于获取所有类别及其相关论坛、
    获取特定类别下的论坛等操作的方法。
    """
    __tablename__ = "categories"

    # 类别的唯一标识符
    id = db.Column(db.Integer, primary_key=True)
    # 类别的标题，不能为空
    title = db.Column(db.String(255), nullable=False)
    # 类别的描述，可为空
    description = db.Column(db.Text, nullable=True)
    # 类别的位置，默认值为1，不能为空
    position = db.Column(db.Integer, default=1, nullable=False)

    # 与论坛的一对多关系
    forums = db.relationship(
        "Forum",
        backref="category",
        lazy="dynamic",
        primaryjoin="Forum.category_id == Category.id",
        order_by="asc(Forum.position)",
        cascade="all, delete-orphan",
    )

    # 特性

    @property
    def slug(self):
        """
        返回类别标题的slug化版本（用于生成友好的URL）。
        """
        return slugify(self.title)

    @property
    def url(self):
        """
        返回类别的slug化URL。
        """
        return url_for("forum.view_category", category_id=self.id, slug=self.slug)

    # 方法

    def __repr__(self):
        """
        返回一个表示类别对象的字符串，格式为<Category {类别ID}>，
        用于调试和日志记录等。
        """
        return "<{} {}>".format(self.__class__.__name__, self.id)

    def delete(self, users=None):
        """
        删除类别。

        首先从数据库中删除类别对象并提交会话。
        如果提供了用户列表（users），则更新这些用户的帖子数量：
        - 遍历用户列表，计算每个用户的帖子数量（通过查询该用户的未隐藏帖子），
        提交数据库会话。
        返回类别对象自身。
        """
        # 删除类别
        db.session.delete(self)
        db.session.commit()

        # 更新用户的帖子数量
        if users:
            for user in users:
                user.post_count = Post.query.filter_by(user_id=user.id).count()
                db.session.commit()

        return self

    # 类方法

    @classmethod
    def get_all(cls, user):
        """
        获取所有类别及其相关论坛。

        如果用户已登录：
        - 获取用户所属组的ID列表。
        - 通过查询筛选出用户组有权限访问的论坛子查询（user_forums）。
        - 使用aliased创建别名（forum_alias）。
        - 通过连接操作和外连接操作，获取类别、论坛及其对应的ForumsRead对象，
        按照类别位置、类别ID、论坛位置排序，返回结果。
        如果用户未登录：
        - 获取访客组。
        - 通过查询筛选出访客组有权限访问的论坛子查询（guest_forums）。
        - 使用aliased创建别名（forum_alias）。
        - 通过连接操作，获取类别和论坛，按照类别位置、类别ID、论坛位置排序，返回结果。
        最后调用get_categories_and_forums函数处理结果并返回。
        """
        from xfree.user.models import Group
        if user.is_authenticated:
            # 获取用户组ID列表
            user_groups = [gr.id for gr in user.groups]
            # 根据用户组筛选论坛
            user_forums = Forum.query.filter(
                Forum.groups.any(Group.id.in_(user_groups))
            ).subquery()

            forum_alias = aliased(Forum, user_forums)
            # 获取所有类别、论坛及其相关的ForumsRead对象
            forums = (
                cls.query.join(forum_alias, cls.id == forum_alias.category_id)
               .outerjoin(
                    ForumsRead,
                    db.and_(
                        ForumsRead.forum_id == forum_alias.id,
                        ForumsRead.user_id == user.id,
                    ),
                )
               .add_entity(forum_alias)
               .add_entity(ForumsRead)
               .order_by(Category.position, Category.id, forum_alias.position)
               .all()
            )
        else:
            # 获取访客组
            guest_group = Group.get_guest_group()
            # 根据访客组筛选论坛
            guest_forums = Forum.query.filter(
                Forum.groups.any(Group.id == guest_group.id)
            ).subquery()

            forum_alias = aliased(Forum, guest_forums)
            forums = (
                cls.query.join(forum_alias, cls.id == forum_alias.category_id)
               .add_entity(forum_alias)
               .order_by(Category.position, Category.id, forum_alias.position)
               .all()
            )

        return get_categories_and_forums(forums, user)

    @classmethod
    def get_forums(cls, category_id, user):
        """
        获取特定类别下的论坛。

        如果用户已登录：
        - 获取用户所属组的ID列表。
        - 通过查询筛选出用户组有权限访问的论坛子查询（user_forums）。
        - 使用aliased创建别名（forum_alias）。
        - 通过过滤、连接操作和外连接操作，获取特定类别下的论坛及其对应的ForumsRead对象，
        按照论坛位置排序，返回结果。
        如果用户未登录：
        - 获取访客组。
        - 通过查询筛选出访客组有权限访问的论坛子查询（guest_forums）。
        - 使用aliased创建别名（forum_alias）。
        - 通过过滤和连接操作，获取特定类别下的论坛，按照论坛位置排序，返回结果。
        如果没有找到论坛，则返回404错误。
        最后调用get_forums函数处理结果并返回。
        """
        from xfree.user.models import Group

        if user.is_authenticated:
            # 获取用户组ID列表
            user_groups = [gr.id for gr in user.groups]
            # 根据用户组筛选论坛
            user_forums = Forum.query.filter(
                Forum.groups.any(Group.id.in_(user_groups))
            ).subquery()

            forum_alias = aliased(Forum, user_forums)
            forums = (
                cls.query.filter(cls.id == category_id)
               .join(forum_alias, cls.id == forum_alias.category_id)
               .outerjoin(
                    ForumsRead,
                    db.and_(
                        ForumsRead.forum_id == forum_alias.id,
                        ForumsRead.user_id == user.id,
                    ),
                )
               .add_entity(forum_alias)
               .add_entity(ForumsRead)
               .order_by(forum_alias.position)
               .all()
            )
        else:
            guest_group = db.Group.get_guest_group()
            # 根据访客组筛选论坛
            guest_forums = Forum.query.filter(
                Forum.groups.any(db.Group.id == guest_group.id)
            ).subquery()

            forum_alias = aliased(Forum, guest_forums)
            forums = (
                cls.query.filter(cls.id == category_id)
               .join(forum_alias, cls.id == forum_alias.category_id)
               .add_entity(forum_alias)
               .order_by(forum_alias.position)
               .all()
            )

        if not forums:
            abort(404)

        return get_forums(forums, user)
